For a while I've been curious to see if there is any effect on a backwardated term structure in Vix Futures on future returns in the SPX.
The VIX measures the price that traders are willing to buy options to protect their portfolio. The spot VIX measures this price. You can buy futures on the VIX. Essentially you are making a bet where the VIX will settle on the date of expiration of the VIX future contract. At settlement you get paid the amount your future is worth. Thus, the futures trade off the price that traders think the index will be at settlement. In times of stress trader run to buy options, pushing the VIX up. Since the VIX is mean reverting this will pull the front month up more than the back month since traders figure that over time the VIX will return to is average levels of about 20. This term structure where the front month is greater than the back month is referred to as backwardation.
However, the "natural" term structure for VIX Futures is contango since they are somewhat tied to the price of SPX options which are naturally more expensive further out in time since there is more uncertainty in the future (even adjusted for time) -- this is why back month options trade at a higher vol (usually) than front month options.
During big down drafts we see the VIX future curve go into steep backwardation.
Does this backwardation happen quickly enough into the drawdown to get you out?
In [1]:
import pandas as pd
import pandas.io.data as web
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import seaborn
from datetime import datetime, timedelta
%matplotlib inline
In [2]:
df = web.DataReader("^GSPC", 'yahoo', datetime(2000, 1, 1), datetime.today())
In [3]:
raw_vix_df = web.DataReader("^VIX", 'yahoo', datetime(2000, 1, 1), datetime.today())
raw_vix_df['vix_close'] = raw_vix_df['Close']
raw_vix_df = raw_vix_df['vix_close'] # drop all the HL shit
In [4]:
vix_df = pd.read_html("http://vixcentral.com/historical/?days=10000", header=0)[0]
vix_df = vix_df.drop(vix_df.index[-1:]) # get rid of last row
vix_df = vix_df.set_index('Date')
vix_df.index = pd.to_datetime(vix_df.index)
In [5]:
df['Close'].plot(figsize=(16, 6))
plt.xlabel('Date', fontsize=14)
plt.ylabel('SPX Close', fontsize=14)
plt.title("Our frienemy, the SPX", fontsize=16)
Out[5]:
In [6]:
# calculate degree of backwardation between month 1 and 2
vix_df['f2-f1'] = vix_df['F2'] - vix_df['F1']
vix_df['backwardated'] = vix_df['f2-f1'] < 0
In [7]:
# join our tables and sort with date ascending
master = df.join(raw_vix_df, how='inner')
master = master.join(vix_df, how='inner')
master = master.sort_index()
In [8]:
# compute rolling returns for 1 day, 5 days, 2 weeks, 1 month, 3 months, 6 months and 12 months
# note this is tricky than it looks -- our dataframe is in acending order,
# so pct_change(periods=1) calculates the chage from day1 to day2, but this change is aligned with day2 so
# we *must* shift is back the same number of periods that the change is calculated over
master['1d'] = master['Close'].pct_change(periods=1).shift(-1) * 100
master['5d'] = master['Close'].pct_change(periods=5).shift(-5) * 100
master['10d'] = master['Close'].pct_change(periods=10).shift(-10) * 100
master['1m'] = master['Close'].pct_change(periods=20).shift(-20) * 100
master['3m'] = master['Close'].pct_change(periods=60).shift(-60) * 100
master['6m'] = master['Close'].pct_change(periods=120).shift(-120) * 100
master['12m'] = master['Close'].pct_change(periods=250).shift(-250) * 100
In [9]:
# verify the pecentage changes look good
master[["Close", "1d", "5d"]]
Out[9]:
In [10]:
# next we'll write a custom formatter
N = len(master.index)
ind = np.arange(N) # the evenly spaced plot indices
def format_date(x, pos=None):
thisind = np.clip(int(x+0.5), 0, N-2)
print(thisind)
return master.iloc[:thisind][0].format().pop()
In [11]:
print(master.iloc[:1].index.format().pop())
In [12]:
fig, ax = plt.subplots()
master['Close'].plot(ax=ax, figsize=(16, 6))
for idx, row in master.iterrows():
if row['backwardated']:
ax.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('SPX Close', fontsize=14)
plt.title("SPX Value with Backwardation Highlighted", fontsize=16)
Out[12]:
In [13]:
plot = master['vix_close'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
if row['backwardated']:
plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('VIX Close', fontsize=14)
plt.title("VIX Close with Backwardation Highlighted", fontsize=16)
Out[13]:
Now, lets dig into the data, segmenting periods of backwardation and contango on the VIX futures curve.
For each rolling return we'll:
In [14]:
plot = master['1d'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
if row['backwardated']:
plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 1 day percentage returns', fontsize=14)
Out[14]:
In [15]:
plt1, plt2 = master['1d'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))
plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 1d Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 1d Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)
Out[15]:
In [16]:
plot = master['5d'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
if row['backwardated']:
plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 5 day percentage returns', fontsize=14)
Out[16]:
In [17]:
plt1, plt2 = master['5d'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))
plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 5d Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 5d Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)
Out[17]:
In [18]:
plot = master['10d'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
if row['backwardated']:
plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 10 day percentage returns', fontsize=14)
Out[18]:
In [19]:
plt1, plt2 = master['10d'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))
plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 10d Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 10d Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)
Out[19]:
In [20]:
plot = master['1m'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
if row['backwardated']:
plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 1 month percentage returns', fontsize=14)
Out[20]:
In [21]:
plt1, plt2 = master['1m'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))
plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 1m Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 1m Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)
Out[21]:
In [22]:
plot = master['3m'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
if row['backwardated']:
plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 3 month percentage returns', fontsize=14)
Out[22]:
In [23]:
plt1, plt2 = master['3m'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))
plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 3m Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 3m Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)
Out[23]:
In [24]:
plot = master['6m'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
if row['backwardated']:
plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 6 month percentage returns', fontsize=14)
Out[24]:
In [25]:
plt1, plt2 = master['6m'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))
plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 6m Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 6m Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)
Out[25]:
In [26]:
plot = master['12m'].plot(figsize=(16, 6))
for idx, row in master.iterrows():
if row['backwardated']:
plot.axvspan(idx, idx+timedelta(days=1), facecolor='red', edgecolor='none', alpha=.2)
plt.xlabel('Date (Red denotes periods of backwardation)', fontsize=14)
plt.ylabel('Rolling 12 month percentage returns', fontsize=14)
Out[26]:
In [27]:
plt1, plt2 = master['12m'].hist(by=master['backwardated'], bins=20, figsize=(16, 6))
plt1.set_title("Contango", fontsize=16)
plt1.set_xlabel('Bucketed 12m Returns', fontsize=14)
plt1.set_ylabel('Ocurrances', fontsize=14)
plt2.set_title("Backwardation", fontsize=16)
plt2.set_xlabel('Bucketed 12m Returns', fontsize=14)
plt2.set_ylabel('Ocurrances', fontsize=14)
Out[27]:
In [28]:
master.groupby(master['backwardated']).describe()[['vix_close', '1d','5d', '10d', '1m', '3m', '6m', '12m']]
Out[28]:
So what did we see?
When the term structure of the VIX is backwardated we see average returns across all time frames actually increase! But, we also have a greater dispersion. This is pretty much a given, since when the VIX is backwadated the Vol level is elevated. We could show this is the case with a correlation analysis, but if you trade you already knew this.
Basically, this means it is a useless signal for the long-only equity trader. You might think that it's actually a inverse signal, but buying the wrong downdraft (i.e. 2008) and you don't get to play again.
For the options trader the question is more complicated since options are much more expensive during these periods, but the moves are much bigger too.
The main problem is that that data set for VIX futures is just too small! We only have ~150 days of backwadadion in the last 6 years. It's hardly enough to go on.
I'll be honest. I'm a premium seller and I'm nervous opening new positions right. Part of me says that this hesitancy is why these trades will pay out. So I'll probably sell some SPX strangles tomorrow, small.
That said, I'm still curious how returns would compare between the premium seller who goes flat during backwadation and the guy who holds on. My guess is that over this data set (2009-present) the guy who held on killed it. But, that same trader would have gotten killed in 2008-2009. I might try to model this using the buy-write index, so be on the lookout for that.
I'd also be curious to segment out the data with respect to the steepness of the curve (i.e., how are return when we are in 1% contago vs -5% backwardation vs 10% backwardation), but honestly you can't draw any good conclusions with this small dataset, so I'll skip it for now.
In [ ]: